/* Copyright (c) 2003 The Nutch Organization. All rights reserved. */
/* Use subject to the conditions in http://www.nutch.org/LICENSE.txt. */
package net.nutch.ipc;
import java.io.IOException;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.net.Socket;
import java.net.ServerSocket;
import java.net.SocketTimeoutException;
import java.util.LinkedList;
import java.util.logging.Logger;
import java.util.logging.Level;
import net.nutch.util.LogFormatter;
import net.nutch.io.Writable;
import net.nutch.io.UTF8;
/** An abstract IPC service. IPC calls take a single {@link Writable} as a
* parameter, and return a {@link Writable} as their value. A service runs on
* a port and is defined by a parameter class and a value class.
*
* @author Doug Cutting
* @see Client
*/
public abstract class Server {
public static final Logger LOG =
LogFormatter.getLogger("net.nutch.ipc.Server");
private int port; // port we listen on
private int handlerCount; // number of handler threads
private int maxQueuedCalls; // max number of queued calls
private Class paramClass; // class of call parameters
private int timeout = 10000; // timeout for i/o
private boolean running = true; // true while server runs
private LinkedList callQueue = new LinkedList(); // queued calls
private Object callDequeued = new Object(); // used by wait/notify
/** A call queued for handling. */
private static class Call {
private int id; // the client's call id
private Writable param; // the parameter passed
private Connection connection; // connection to client
public Call(int id, Writable param, Connection connection) {
this.id = id;
this.param = param;
this.connection = connection;
}
}
/** Listens on the socket, starting new connection threads. */
private class Listener extends Thread {
private ServerSocket socket;
public Listener() throws IOException {
this.socket = new ServerSocket(port);
socket.setSoTimeout(timeout);
this.setDaemon(true);
this.setName("Server listener on port " + port);
}
public void run() {
LOG.info(getName() + ": starting");
while (running) {
try {
new Connection(socket.accept()).start(); // start a new connection
} catch (SocketTimeoutException e) { // ignore timeouts
} catch (Exception e) { // log all other exceptions
LOG.log(Level.INFO, getName() + " caught: " + e, e);
}
}
try {
socket.close();
} catch (IOException e) {}
LOG.info(getName() + ": exiting");
}
}
/** Reads calls from a connection and queues them for handling. */
private class Connection extends Thread {
private Socket socket;
private DataInputStream in;
private DataOutputStream out;
public Connection(Socket socket) throws IOException {
this.socket = socket;
socket.setSoTimeout(timeout);
this.in = new DataInputStream
(new BufferedInputStream(socket.getInputStream()));
this.out = new DataOutputStream
(new BufferedOutputStream(socket.getOutputStream()));
this.setDaemon(true);
this.setName("Server connection on port " + port + " from "
+ socket.getInetAddress().getHostAddress());
}
public void run() {
LOG.info(getName() + ": starting");
try {
while (running) {
int id;
try {
id = in.readInt(); // try to read an id
} catch (SocketTimeoutException e) {
continue;
}
if (LOG.isLoggable(Level.FINE))
LOG.fine(getName() + " got #" + id);
Writable param = makeParam(); // read param
param.readFields(in);
Call call = new Call(id, param, this);
synchronized (callQueue) {
callQueue.addLast(call); // queue the call
callQueue.notify(); // wake up a waiting handler
}
while (running && callQueue.size() >= maxQueuedCalls) {
synchronized (callDequeued) { // queue is full
callDequeued.wait(timeout); // wait for a dequeue
}
}
}
} catch (Exception e) {
LOG.log(Level.INFO, getName() + " caught: " + e, e);
} finally {
try {
socket.close();
} catch (IOException e) {}
LOG.info(getName() + ": exiting");
}
}
}
/** Handles queued calls . */
private class Handler extends Thread {
public Handler() {
this.setDaemon(true);
this.setName("Server handler on " + port);
}
public void run() {
LOG.info(getName() + ": starting");
while (running) {
try {
Call call;
synchronized (callQueue) {
while (running && callQueue.size()==0) { // wait for a call
callQueue.wait(timeout);
}
if (!running) break;
call = (Call)callQueue.removeFirst(); // pop the queue
}
synchronized (callDequeued) { // tell others we've dequeued
callDequeued.notify();
}
if (LOG.isLoggable(Level.FINE))
LOG.fine(getName() + ": has #" + call.id + " from " +
call.connection.socket.getInetAddress().getHostAddress());
String error = null;
Writable value = null;
try {
value = call(call.param); // make the call
} catch (Exception e) {
LOG.log(Level.INFO, getName() + " call error: " + e, e);
error = e.toString();
}
DataOutputStream out = call.connection.out;
synchronized (out) {
out.writeInt(call.id); // write call id
out.writeBoolean(error!=null); // write error flag
if (error != null)
value = new UTF8(error);
value.write(out); // write value
out.flush();
}
} catch (Exception e) {
LOG.log(Level.INFO, getName() + " caught: " + e, e);
}
}
LOG.info(getName() + ": exiting");
}
}
/** Constructs a server listening on the named port. Parameters passed must
* be of the named class. The <code>handlerCount</handlerCount> determines
* the number of handler threads that will be used to process calls.
*/
protected Server(int port, Class paramClass, int handlerCount) {
this.port = port;
this.paramClass = paramClass;
this.handlerCount = handlerCount;
this.maxQueuedCalls = handlerCount;
}
/** Sets the timeout used for network i/o. */
public void setTimeout(int timeout) { this.timeout = timeout; }
/** Starts the service. Must be called before any calls will be handled. */
public synchronized void start() throws IOException {
Listener listener = new Listener();
listener.start();
for (int i = 0; i < handlerCount; i++) {
Handler handler = new Handler();
handler.start();
}
}
/** Stops the service. No calls will be handled after this is called. All
* threads will exit. */
public synchronized void stop() {
LOG.info("Stopping server on " + port);
running = false;
try {
Thread.sleep(timeout); // let all threads exit
} catch (InterruptedException e) {}
notify();
}
/** Wait for the server to be stopped. */
public synchronized void join() throws InterruptedException {
wait();
}
/** Called for each call. */
public abstract Writable call(Writable param) throws IOException;
private Writable makeParam() {
Writable param; // construct param
try {
param = (Writable)paramClass.newInstance();
} catch (InstantiationException e) {
throw new RuntimeException(e.toString());
} catch (IllegalAccessException e) {
throw new RuntimeException(e.toString());
}
return param;
}
}